import { Dimension } from '../common/dimension';
import { Vector } from '../common/vector';

import { IDirectDrawParams } from './direct-draw-params.interface';
import { IDrawParams } from './draw-params.interface';
import { IRenderingContextConfig } from './rendering-context-config.interface';

const DEF_DIMENSION_IN_UNITS = new Dimension(640, 480);

/**
 * Класс для рисования, который абстрагируется от пикселей и позволяет рисовать в условных единицах измерения (юнитах).
 * Также класс позволяет добавлять черные рамки, если разрешение рендера не совпадает с соотношением сторон контекста.
 */
export class RenderingContext {
  #dimensionInPixels: Dimension;
  #dimensionInUnits: Dimension;

  #indent: Vector;
  #multiplier: Vector;
  #adjustedDimensionInPixels: Dimension;

  #context: CanvasRenderingContext2D;

  get adjustedDimensionInPixels(): Dimension {
    return this.#adjustedDimensionInPixels;
  }

  get dimensionInPixels(): Dimension {
    return this.#dimensionInPixels;
  }

  get dimensionInUnits(): Dimension {
    return this.#dimensionInUnits;
  }

  get indent(): Vector {
    return this.#indent;
  }

  get multiplier(): Vector {
    return this.#multiplier;
  }

  constructor(
    private readonly canvasElement: HTMLCanvasElement,
    private readonly config: IRenderingContextConfig
  ) {
    this.initConfig();
    this.initCanvasElement();
    this.initContext();
    this.recalculate();
  }

  castUnitLocationToPixel(locationInUnits: Vector): Vector {
    const { x: indentX, y: indentY } = this.indent;
    const { x: multiplierX, y: multiplierY } = this.multiplier;

    return new Vector(locationInUnits.x * multiplierX + indentX, locationInUnits.y * multiplierY + indentY);
  }

  castUnitDimensionToPixel(dimensionInUnits: Dimension): Dimension {
    const { x: multiplierX, y: multiplierY } = this.multiplier;

    return new Dimension(dimensionInUnits.width * multiplierX, dimensionInUnits.height * multiplierY);
  }

  castPixelLocationToUnit(locationInPixels: Vector): Vector {
    const { x: indentX, y: indentY } = this.indent;
    const { x: multiplierX, y: multiplierY } = this.multiplier;

    return new Vector((locationInPixels.x - indentX) / multiplierX, (locationInPixels.y - indentY) / multiplierY);
  }

  castPixelDimensionToUnit(dimensionInPixels: Dimension): Dimension {
    const { x: multiplierX, y: multiplierY } = this.multiplier;

    return new Dimension(dimensionInPixels.width / multiplierX, dimensionInPixels.height / multiplierY);
  }

  changeDimensionInUnits(dimensionInUnits: Dimension) {
    this.#dimensionInUnits = dimensionInUnits;

    this.recalculate();
  }

  changeDimensionInPixels(dimensionInPixels: Dimension) {
    this.#dimensionInPixels = dimensionInPixels;

    this.recalculate();
  }

  clear() {
    const { dimensionInPixels, config } = this;

    this.#context.fillStyle = config.clearFillStyle;
    this.#context.fillRect(0, 0, dimensionInPixels.width, dimensionInPixels.height);
  }

  drawBars() {
    const { dimensionInPixels, indent, config } = this;

    this.#context.fillStyle = config.barFillStyle;

    /// top bar
    this.#context.fillRect(0, 0, dimensionInPixels.width, indent.y);

    /// right bar
    this.#context.fillRect(dimensionInPixels.width - indent.x, 0, indent.x, dimensionInPixels.height);

    /// bottom bar
    this.#context.fillRect(0, dimensionInPixels.height - indent.y, dimensionInPixels.width, indent.y);

    /// left bar
    this.#context.fillRect(0, 0, indent.x, dimensionInPixels.height);
  }

  drawImage(image: HTMLImageElement, targetLocationInUnits: Vector, targetDimensionInUnits: Dimension) {
    const params: IDrawParams = {
      image,
      sourceLocationInPixels: new Vector(0, 0),
      sourceDimensionInPixels: new Dimension(image.width, image.height),
      targetLocationInUnits,
      targetDimensionInUnits,
    };

    this.drawSprite(params);
  }

  drawSprite(params: IDrawParams) {
    const {
      image,
      sourceLocationInPixels,
      sourceDimensionInPixels,
      targetLocationInUnits: tl,
      targetDimensionInUnits: td,
    } = params;

    const targetLocationInPixels = this.castUnitLocationToPixel(tl);
    const targetDimensionInPixels = this.castUnitDimensionToPixel(td);

    const directParams: IDirectDrawParams = {
      image,
      sourceLocationInPixels,
      sourceDimensionInPixels,
      targetLocationInPixels,
      targetDimensionInPixels,
    };

    this.drawSpriteDirectly(directParams);
  }

  drawSpriteDirectly(params: IDirectDrawParams) {
    const {
      image: img,
      sourceLocationInPixels: sl,
      sourceDimensionInPixels: sd,
      targetLocationInPixels: tl,
      targetDimensionInPixels: td,
    } = params;

    this.#context.drawImage(img, sl.x, sl.y, sd.width, sd.height, tl.x, tl.y, td.width, td.height);
  }

  drawRectangle(locationInUnits: Vector, dimensionInUnits: Dimension, fillStyle: string) {
    const locationInPixels = this.castUnitLocationToPixel(locationInUnits);
    const dimensionInPixels = this.castUnitDimensionToPixel(dimensionInUnits);

    this.drawRectangleDirectly(locationInPixels, dimensionInPixels, fillStyle);
  }

  drawRectangleDirectly(locationInPixels: Vector, dimensionInPixels: Dimension, fillStyle: string) {
    this.#context.fillStyle = fillStyle;
    this.#context.fillRect(locationInPixels.x, locationInPixels.y, dimensionInPixels.width, dimensionInPixels.height);
  }

  drawBorder(locationInUnits: Vector, dimensionInUnits: Dimension, strokeStyle: string) {
    const locationInPixels = this.castUnitLocationToPixel(locationInUnits);
    const dimensionInPixels = this.castUnitDimensionToPixel(dimensionInUnits);

    this.drawBorderDirectly(locationInPixels, dimensionInPixels, strokeStyle);
  }

  drawBorderDirectly(locationInPixels: Vector, dimensionInPixels: Dimension, strokeStyle: string) {
    this.#context.strokeStyle = strokeStyle;
    this.#context.strokeRect(locationInPixels.x, locationInPixels.y, dimensionInPixels.width, dimensionInPixels.height);
  }

  private recalculate() {
    const { dimensionInPixels, dimensionInUnits } = this;

    const adjustedWidth = (dimensionInPixels.height * dimensionInUnits.width) / dimensionInUnits.height;
    if (adjustedWidth <= dimensionInPixels.width) {
      this.#adjustedDimensionInPixels = new Dimension(adjustedWidth, dimensionInPixels.height);

      const indentX = (dimensionInPixels.width - adjustedWidth) / 2;
      this.#indent = new Vector(indentX, 0);
    } else {
      const adjustedHeight = (dimensionInPixels.width * dimensionInUnits.height) / dimensionInUnits.width;
      if (adjustedHeight <= dimensionInPixels.height) {
        this.#adjustedDimensionInPixels = new Dimension(dimensionInPixels.width, adjustedHeight);

        const indentY = (dimensionInPixels.height - adjustedHeight) / 2;
        this.#indent = new Vector(0, indentY);
      } else {
        throw new Error('Cannot recalculate rendering resolution');
      }
    }

    const { adjustedDimensionInPixels } = this;

    const multiplierX = adjustedDimensionInPixels.width / dimensionInUnits.width;
    const multiplierY = adjustedDimensionInPixels.height / dimensionInUnits.height;
    this.#multiplier = new Vector(multiplierX, multiplierY);
  }

  private initConfig() {
    let { initialDimensionInUnits, initialDimensionInPixels } = this.config;

    if (!initialDimensionInUnits) {
      initialDimensionInUnits = DEF_DIMENSION_IN_UNITS;
    }

    if (!initialDimensionInPixels) {
      const parentElement = this.canvasElement.parentElement;

      if (parentElement) {
        initialDimensionInPixels = new Dimension(parentElement.clientWidth, parentElement.clientHeight);
      } else {
        initialDimensionInPixels = DEF_DIMENSION_IN_UNITS;
      }
    }

    const { width: widthInUnits, height: heightInUnits } = initialDimensionInUnits;
    this.#dimensionInUnits = new Dimension(widthInUnits, heightInUnits);

    const { width: widthInPixels, height: heightInPixels } = initialDimensionInPixels;
    this.#dimensionInPixels = new Dimension(widthInPixels, heightInPixels);
  }

  private initCanvasElement() {
    const { width: widthInPixels, height: heightInPixels } = this.dimensionInPixels;

    this.canvasElement.width = widthInPixels;
    this.canvasElement.height = heightInPixels;
  }

  private initContext() {
    const context = this.canvasElement.getContext('2d');

    if (!context) {
      throw new Error('Cannot get 2d context');
    }

    this.#context = context;
  }
}
